// 日志落地类
#ifndef __MY_SINK__
#define __MY_SINK__

#include <iostream>
#include <memory>
#include <assert.h>
#include <sstream>
#include <fstream>

#include "util.hpp"

namespace ken_log
{
    // 抽象出落地模块类，抽象出基类
    class LogSink
    {
    public:
        // 后来通过指针进行访问，所以定义一个智能指针
        using ptr = std::shared_ptr<LogSink>;
        // 构造函数可有可无
        LogSink() {}
        // 用户进行扩展的时候可能会在子类析构中释放一些资源，所以要将析构函数定义成虚函数
        virtual ~LogSink() {}
        // 日志落地功能，参数为数据落地的起始位置、落地数据的长度
        virtual void log(const char *data, size_t len) = 0;
    };

    // 三个落地方向：1、标准输出 2、指定文件 3、滚动文件（以大小进行滚动）
    // 派生出标准输出的派生类
    class StdoutSink : public LogSink
    {
    public:
        // 在类中对虚函数进行重写
        void log(const char *data, size_t len) override
        {
            // 这里使用write进行写入，<<没办法指定大小
            std::cout.write(data, len);
        }
    };
    
    // 2、派生出向指定文件输出的派生类
    class FileSink : public LogSink
    {
    public:
        // 构造的时候要知道要写入的文件名,通过操作句柄打开文件
        FileSink(const std::string &pathname)
            : _pathname(pathname) // 初始化文件名
        {
            // 在构造函数直接打开文件，可能文件所在的路径不存在
            // 1、创建日志文件所在的目录，使用path获取文件目录
            ken_log::util::File::createDirectory(ken_log::util::File::path(pathname));
            // 2、创建并打开日志文件，ofstream当文件不存在的时候会自动创建文件
            // std::ios::binary以二进制方式打开文件
            // 我们每次写的时候都应该是追加字符串，所以以std::ios::app方式
            // 打开文件
            _ofs.open(_pathname, std::ios::binary | std::ios::app);
            assert(_ofs.is_open()); // 判断文件是否打开
        }
        // 在类中对虚函数进行重写
        void log(const char *data, size_t len) override
        {
            // 进行文件的写入
            _ofs.write(data, len);
            // 判断下句柄操作句柄是否正常
            assert(_ofs.good());
        }

    private:
        // 成员变量要有其具体的文件路径名和对文件进行操作的操作句柄
        std::string _pathname; // 文件路径名
        std::ofstream _ofs;    // 对输出文件进行管理的一个句柄，头文件fstream，把内容写到文件中使用ofstream
    };
    
    // 3、向滚动文件中进行输出（以大小进行滚动，当文件达到指定大小创建新文件进行输出）
    class RollBySizeSink : public LogSink
    {
    public:
        // 构造的时候传入文件名并打开文件,通过操作句柄管理文件
        RollBySizeSink(const std::string &basename, size_t max_size)
            : _basename(basename), _max_size(max_size), _cur_size(0) // 当前文件大小也要初始化
              ,
              _name_num(0)
        {
            // 在构造函数中去打开这个文件,在createNewFile进行文件的创建
            std::string pathname = createNewFile();
            _name_num++;
            // 1、创建日志文件所在的目录,使用path获取文件目录
            util::File::createDirectory(util::File::path(pathname));
            // 打开文件
            _ofs.open(pathname, std::ios::binary | std::ios::app);
            // 判断文件是否打开
            assert(_ofs.is_open());
        }
        // 将日志消息写入到滚动文件中
        void log(const char *data, size_t len) override
        {
            // 1、首先就要判断当前文件大小是否大于文件最大大小
            if (_max_size <= _cur_size)
            {
                // 关闭当前文件
                _ofs.close();
                // 创建并打开新文件
                std::string pathname = createNewFile();
                _ofs.open(pathname, std::ios::binary | std::ios::app);
                assert(_ofs.is_open());
                _cur_size = 0;
                _name_num++;
            }
            // 数据的写入
            _ofs.write(data, len);
            // 判断当前文件是否操作正常，写入是否失败
            assert(_ofs.good());
            _cur_size += len;
        }

    private:
        // 创建新文件，通过基础文件名+扩展名生成新文件名
        std::string createNewFile()
        {
            // 1、首先要获取系统时间
            time_t t = util::Date::now_time();
            struct tm lt;         // lt用来接收转换后的时间结构
            localtime_r(&t, &lt); // 把结构化的时间信息放到lt中
            // 使用一个字符串流,直接输入到字符串流中
            std::stringstream filename;
            filename << _basename;
            filename << lt.tm_year + 1900;
            filename << lt.tm_mon + 1;
            filename << lt.tm_mday;
            filename << lt.tm_hour;
            filename << lt.tm_min;
            filename << lt.tm_sec;
            filename << "-";
            filename << _name_num;
            filename << ".log"; // 文件后缀名
            // 通过str接口获取成员对象
            return filename.str();
        }

    private:
        std::string _basename; // 文件基础名
        std::ofstream _ofs;    // 文件操作句柄
        size_t _max_size;      // 文件最大大小
        size_t _cur_size;      // 文件当前大小
        size_t _name_num;      // 避免文件写入过快导致的文件名相同
    };
    
    // 日志工厂类
    class SinkFactory
    {
    public:
        // 通过模板参数传递过来一个日志落地类型，但是因为落地类中的构造函数参数不同所以使用函数模板
        template <class SinkType, class... Args>
        // 通过右值引用
        static LogSink::ptr create(Args &&...args)
        {
            // 使用完美转发
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }
    };
}
#endif
